<?php
// Copyright 2014 CloudHarmony Inc.
// 
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// 
//     http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


/**
 * Used to manage SPEC CPU 2006 testing
 */
require_once(dirname(__FILE__) . '/util.php');
ini_set('memory_limit', '16m');
date_default_timezone_set('UTC');

class SpecCpu2006Test {
  
  /**
   * default directory to check for spec install
   */
  const SPEC_CPU_2006_DEFAULT_DIR = '/opt/cpu2006';
  
  /**
   * default run timeout (72 hours)
   */
  const SPEC_CPU_2006_DEFAULT_TIMEOUT = 259200;
  
  /*
   * estimated disk usage per copy (2GB)
   */
  const SPEC_CPU_2006_ESTIMATED_DISK_USAGE_MB = 2048;
  
  /*
   * path to the huge pages library for 32-bit execution
   */
  const SPEC_CPU_2006_HUGE_PAGES_LIB32 = '/usr/lib/libhugetlbfs.so';

  /*
   * path to the huge pages library for 64-bit execution
   */
  const SPEC_CPU_2006_HUGE_PAGES_LIB64 = '/usr/lib64/libhugetlbfs.so';
  
  /*
   * estimated disk usage per copy (2GB)
   */
  const SPEC_CPU_2006_RUN_OUTPUT_FILE = 'spec.log';
  
  /*
   * name of the run script (generated by generateRunScript)
   */
  const SPEC_CPU_2006_RUN_SCRIPT = 'specrun';
  
  /* 
   * file that is created to designate sse failure when failover_no_sse is set
   */
  const SPEC_CPU_2006_SKIP_SSE_FILE = '.sse_failover';
  
  /**
   * name of the file where serializes options should be written to for given 
   * test iteration
   */
  const SPEC_CPU_2006_TEST_OPTIONS_FILE_NAME = '.options';
  
  /*
   * file that is created to designate x64 failure when x64_failover is set
   */
  const SPEC_CPU_2006_X64_FAILOVER_FILE = '.x64_failover';
  
  
  /**
   * optional results directory object was instantiated for
   */
  private $dir;
  
  /**
   * run options
   */
  private $options;
  
  /**
   * verbose debug flag
   */
  private $verbose;
  
  
  /**
   * constructor
   * @param string $dir optional results directory object is being instantiated
   * for. If set, runtime parameters will be pulled from the .options file. Do
   * not set when running a test
   */
  public function SpecCpu2006Test($dir=NULL) {
    $this->dir = $dir;
  }
  
  /**
   * writes test results and finalizes testing
   * @return boolean
   */
  private function endTest() {
    $ended = FALSE;
    $dir = $this->options['output'];
    
    // add test stop time
    $this->options['test_stopped'] = date('Y-m-d H:i:s');
    
    // serialize options
    $ofile = sprintf('%s/%s', $dir, self::SPEC_CPU_2006_TEST_OPTIONS_FILE_NAME);
    if (is_dir($dir) && is_writable($dir)) {
      $fp = fopen($ofile, 'w');
      fwrite($fp, serialize($this->options));
      fclose($fp);
      $ended = TRUE;
    }
    
    return $ended;
  }
  
  /**
   * generates the runscript - returns the path to the script on success
   * @param string $runspec the runspec command arguments
   * @param string $sse SSE option
   * @param boolean $x64 is 64 bit run?
   * @return string
   */
  private function generateRunScript($runspec, $sse, $x64) {
  	$script = sprintf('%s/%s%s%s', $this->options['output'], self::SPEC_CPU_2006_RUN_SCRIPT, $sse ? '_' . $sse : '', $x64 ? '_x64' : '');
  	if ($fp = fopen($script, 'w')) {
  	  $this->runCounter++;
  		$generated = TRUE;
  		fwrite($fp, "#!/bin/bash\n");
  		fwrite($fp, 'export SPEC=' . $this->options['spec_dir'] . "\n");
  		fwrite($fp, 'cd ' . $this->options['spec_dir'] . "\n");
  		fwrite($fp, "source shrc\n");
  		fwrite($fp, "ulimit -s unlimited\n");
  		// Huge pages
  		if ($this->options['huge_pages'] && file_exists($hugePagesLib = file_exists(self::SPEC_CPU_2006_HUGE_PAGES_LIB64) ? self::SPEC_CPU_2006_HUGE_PAGES_LIB64 : self::SPEC_CPU_2006_HUGE_PAGES_LIB32)) {
  			// check if huge pages are available
  			if (preg_match('/([0-9]+)$/msU', shell_exec('cat /proc/meminfo | grep HugePages_Free'), $m) && $m[1] > 0) {
  			  print_msg('Huge pages will be enabled because ' . $m[1] . ' are available', $this->verbose, __FILE__, __LINE__);
  				fwrite($fp, "export HUGETLB_MORECORE=yes\n");
  				fwrite($fp, "export LD_PRELOAD=${hugePagesLib}\n");
  			}
  			else print_msg('Huge pages will not be enabled because there are none free as determined from cat /proc/meminfo | grep HugePages_Free', $this->verbose, __FILE__, __LINE__);
  		}
      
  		fwrite($fp, sprintf("%srunspec %s\n", isset($macros['numa']) && $macros['numa'] ? 'numactl --interleave=all ' : '', $runspec));
  		fwrite($fp, "echo $?\n");
  		fclose($fp);
  		exec(sprintf('chmod 755 %s', $script));
  	}
  	else $script = NULL;
  	
  	return $script;
  }
  
  /**
   * returns the full name of all the benchmarks being run
   * @return array
   */
  public function getBenchmarks() {
    $config = string_to_hash(file_get_contents(dirname(dirname(__FILE__)) . '/config/spec-benchmarks.ini'), TRUE);
    $benchmarks = array();
    foreach(array('int' => 'cint2006', 'fp' => 'cfp2006') as $id => $key) {
      foreach(array_keys($config[$key]) as $benchmark) {
        $pieces = explode('.', $benchmark);
        if (in_array('all', $this->options['benchmark']) || in_array($id, $this->options['benchmark']) || in_array($pieces[0], $this->options['benchmark'])) $benchmarks[] = $benchmark;
      }
    }
    return $benchmarks;
  }
  
  /**
   * returns the number of copies to include in the run (based on the copies run
   * parameter) - applies only to rate runs
   * @param string $copies the copies designation string (see README)
   * @param int $max the max copies
   * @return int
   */
  private function getNumCopies($param, $max) {
  	$copies = NULL;
  	$working = array();
  	foreach(explode('|', $param) as $param) {
  		$x64 = FALSE;
  		if (preg_match('/^x64:(.*)$/', $param, $m)) {
  			$x64 = TRUE;
  			$param = $m[1];
  		}
  		$pieces = explode('/', trim($param));
  		$highest = FALSE;
  		if (substr($pieces[0], 0, 1) == '+') {
  			$highest = TRUE;
  			$pieces[0] = str_replace('+', '', $pieces[0]);
  		}
  		$working[$x64] = array('highest' => $highest, 'param' => $param, 'pieces' => $pieces);
  	}
  	// determine whether or use 32 or 64 bit parameter value
  	if (count($keys = array_keys($working)) > 1) $key = is_64bit() && $this->options['x64'] ? 1 : 0;
  	else $key = $keys[0];
  	$highest = $working[$key]['highest'];
  	$param = $working[$key]['param'];
  	$pieces = $working[$key]['pieces'];
    $sysInfo = get_sys_info();
    
  	print_msg("Calculating number of copies to run using copies parameter $param [highest=$highest]", $this->verbose, __FILE__, __LINE__);

  	foreach($pieces as $c) {
  		$c = trim($c);
  		$pcopies = NULL;
  		// cpu core relative
  		if (preg_match('/^([0-9\.]+)%$/', $c, $m) && $sysInfo['cpu_cores']) {
  			$pcopies = round($sysInfo['cpu_cores'] * $m[1] * .01);
  			print_msg("Calculated CPU relative count $pcopies from $c", $this->verbose, __FILE__, __LINE__);
  		}
  		// memory relative
  		else if (preg_match('/^([0-9\.]+)([GMBgmb]+)$/', $c, $m) && isset($sysInfo['memory_mb'])) {
  			$pcopies = round($sysInfo['memory_mb']/($m[1]*(strtolower(trim($m[2])) == 'gb' ? 1024 : 1)));
  			print_msg("Calculated memory relative count $pcopies from $c", $this->verbose, __FILE__, __LINE__);
  		}
  		// fixed
  		else if (preg_match('/^([0-9]+)$/', $c, $m)) {
  			$pcopies = $m[1];
  			print_msg("Found fixed count $pcopies from $c", $this->verbose, __FILE__, __LINE__);
  		}

  		if ($pcopies !== NULL && $pcopies < 1) $pcopies = 1;
  		if ($pcopies && (!$copies || ($highest && $pcopies > $copies) || (!$highest && $pcopies < $copies))) {
  			$copies = $pcopies;
  			print_msg("Adjusting copies to $pcopies", $this->verbose, __FILE__, __LINE__);
  		}
  	}
  	$copies = $copies ? $copies : 1;

  	// max copies
  	if (is_numeric($max) && $copies > $max) {
  		print_msg("Reducing copies from $copies to $max due to max_copies constraint", $this->verbose, __FILE__, __LINE__);
  		$copies = $max;
  	}

  	print_msg("Returning copies=$copies", $this->verbose, __FILE__, __LINE__);
  	return $copies;
  }
  
  /**
   * returns results from testing as a hash of key/value pairs. If results are
   * not available, returns NULL
   * @param boolean $base whether to return base or peak results
   * @param boolean $verbose show verbose output
   * @return array
   */
  public function getResults($base=TRUE, $verbose=NULL) {
    $results = NULL;
    if (is_dir($this->dir) && self::getSerializedOptions($this->dir) && $this->getRunOptions()) {
      $numBenchmarks = 0;
      $offset = $base ? 0 : 5;
      $verbose = $verbose !== NULL ? $verbose : $this->verbose;
      $results = array();
      foreach($this->options as $key => $val) {
        $col = $key;
        if ($col == 'benchmark') $col = 'benchmarks';
        $results[$col] = is_array($val) ? implode('|', $val) : $val;
      }
      foreach(array('specint2006.csv', 'specfp2006.csv') as $csv) {
        if (file_exists($csv = sprintf('%s/%s', $this->options['output'], $csv))) {
          print_msg(sprintf('Processing csv results file %s for %s metrics', $csv, $base ? 'base' : 'peak'), $verbose, __FILE__, __LINE__);
          // section identifier: 
          // 0=header; 1=full results table; 
          // 2=selected results table; 3=SPEC metrics
          $section = 0;
          $rate = $this->options['rate'] == 1;
          foreach(file($csv) as $line) {
            $row = explode(',', trim($line));
            if ($section == 0 && preg_match('/valid/i', $row[0])) $results['valid'] = $row[1] == 1;
            else if (preg_match('/^Benchmark/', $row[0])) {
              $section++;
              $rate = preg_match('/Copies/', $row[1]) ? TRUE : FALSE;
              $results['rate'] = $rate;
            }
            else if ($section == 1 && count($row) == 12 && preg_match('/^[1-9]/', $row[0])) {
              if (is_numeric($row[$offset + 3]) && $row[$offset + 3] > 0) {
                $prefix = 'benchmark_' . strtolower(str_replace('.', '_', $row[0]));
                if ($rate) $results['copies'] = $row[$offset + 1]*1;
                else $results[$prefix . '_reftime'] = $row[$offset + 1]*1;
                
                // selected metric
                if ($row[$offset + 4] == 1) {
                  $results[$prefix] = $row[$offset + 3]*1;
                  $results[$prefix . '_runtime'] = $row[$offset + 2]*1;
                  print_msg(sprintf('Adding %s %s %s metric: %s', $row[0], $base ? 'base' : 'peak', $rate ? 'rate' : 'speed', $results[$prefix]), $verbose, __FILE__, __LINE__);
                }
                if (!isset($results[$prefix . '_metrics'])) $results[$prefix . '_metrics'] = array();
                $results[$prefix . '_metrics'][] = $row[$offset + 3]*1;
              }
              else print_msg(sprintf('Skipping %s %s %s metric because value at column %d (%s) in line [%s] is not numeric', $row[0], $base ? 'base' : 'peak', $rate ? 'rate' : 'speed', $offset + 4, $row[$offset + 3], trim($line)), $verbose, __FILE__, __LINE__);
            }
            else if ($section == 2 && !trim($line)) $section++;
            // SPECint or SPECfp metric
            else if ($section == 3 && preg_match('/^SPEC/', $row[0])) {
              $metric = NULL;
              for($i=1; $i<count($row); $i++) {
                if (is_numeric($row[$i]) && $row[$i] > 0) $metric = $row[$i]*1;
              }
              if ($metric) {
                print_msg(sprintf('%s metric: %s', $row[0], $metric), $verbose, __FILE__, __LINE__);
                $results[strtolower($row[0])] = $metric;
              }
              else print_msg(sprintf('%s metric not present', $row[0]), $verbose, __FILE__, __LINE__);
            }
            // calculate min, max and stdev for individual benchmark metrics
            foreach(array_keys($results) as $col) {
              if (preg_match('/^(.*)_metrics$/', $col, $m)) {
                $numBenchmarks++;
                $metrics = $results[$col];
                unset($results[$col]);
                sort($metrics);
                $results[$m[1] . '_min'] = $metrics[0];
                $results[$m[1] . '_max'] = $metrics[count($metrics) - 1];
                $results[$m[1] . '_stdev'] = get_std_dev($metrics);
                print_msg(sprintf('Calculated min, max and standard deviation metrics [%s; %s; %s] for %s', $results[$m[1] . '_min'], $results[$m[1] . '_max'], $results[$m[1] . '_stdev'], $m[1]), $verbose, __FILE__, __LINE__);
              }
            }
          }
        }
        else print_msg(sprintf('Skipping csv results file %s because it is not present', $csv), $verbose, __FILE__, __LINE__);
      }
      // at least 1 benchmark result
      if ($numBenchmarks) {
        $results['num_benchmarks'] = $numBenchmarks;
        print_msg(sprintf('Successfully parsed %s SPEC CPU 2006 metrics based on %d benchmarks', $base ? 'base' : 'peak', $numBenchmarks), $verbose, __FILE__, __LINE__);
        ksort($results);
      }
      // no benchmark results
      else {
        print_msg(sprintf('Unable to parse %s SPEC CPU 2006 metrics', $base ? 'base' : 'peak'), $verbose, __FILE__, __LINE__);
        $results = NULL;
      }
    }
    return $results;
  }
  
  /**
   * returns run options represents as a hash
   * @return array
   */
  public function getRunOptions() {
    if (!isset($this->options)) {
      if ($this->dir) $this->options = self::getSerializedOptions($this->dir);
      else {
        // default run argument values
        $sysInfo = get_sys_info();
        $defaults = array(
          'benchmark' => 'all',
          'collectd_rrd_dir' => '/var/lib/collectd/rrd',
          'copies' => 'x64:100%/2GB|100%/1GB',
          'failover_no_sse' => 0,
          'huge_pages' => 0,
          'ignore_errors' => 0,
          'iterations' => 3,
          'meta_cpu' => $sysInfo['cpu'],
          'meta_memory' => $sysInfo['memory_gb'] > 0 ? $sysInfo['memory_gb'] . ' GB' : $sysInfo['memory_mb'] . ' MB',
          'meta_os' => $sysInfo['os_info'],
          'nobuild' => 1,
          'nocleanup' => 0,
          'nonuma' => 0,
          'output' => trim(shell_exec('pwd')),
          'purge_output' => 1,
          'rate' => 1,
          'reportable' => 0,
          'review' => 0,
          'run_timeout' => self::SPEC_CPU_2006_DEFAULT_TIMEOUT,
          'size' => 'ref',
          'sse' => 'optimal',
          'sse_max' => 'AVX2',
          'sse_min' => 'SSE4.2',
          'tune' => 'base',
          'validate_disk_space' => 1,
          'x64' => 2,
          'x64_failover' => 0
        );
        $opts = array(
          'benchmark:',
          'collectd_rrd',
          'collectd_rrd_dir:',
          'comment:',
          'config:',
          'copies:',
          'delay:',
          'failover_no_sse:',
          'flagsurl:',
          'huge_pages',
          'ignore_errors',
          'iterations:',
          'max_copies:',
          'meta_burst:',
          'meta_compiler:',
          'meta_compute_service:',
          'meta_compute_service_id:',
          'meta_cpu:',
          'meta_instance_id:',
          'meta_hw_avail:',
          'meta_hw_fpu:',
          'meta_hw_nthreadspercore:',
          'meta_hw_other:',
          'meta_hw_ocache:',
          'meta_hw_pcache:',
          'meta_hw_tcache:',
          'meta_hw_ncpuorder:',
          'meta_license_num:',
          'meta_memory:',
          'meta_notes_1:',
          'meta_notes_2:',
          'meta_notes_3:',
          'meta_notes_4:',
          'meta_notes_5:',
          'meta_notes_base_1:',
          'meta_notes_base_2:',
          'meta_notes_base_3:',
          'meta_notes_base_4:',
          'meta_notes_base_5:',
          'meta_notes_comp_1:',
          'meta_notes_comp_2:',
          'meta_notes_comp_3:',
          'meta_notes_comp_4:',
          'meta_notes_comp_5:',
          'meta_notes_os_1:',
          'meta_notes_os_2:',
          'meta_notes_os_3:',
          'meta_notes_os_4:',
          'meta_notes_os_5:',
          'meta_notes_part_1:',
          'meta_notes_part_2:',
          'meta_notes_part_3:',
          'meta_notes_part_4:',
          'meta_notes_part_5:',
          'meta_notes_peak_1:',
          'meta_notes_peak_2:',
          'meta_notes_peak_3:',
          'meta_notes_peak_4:',
          'meta_notes_peak_5:',
          'meta_notes_plat_1:',
          'meta_notes_plat_2:',
          'meta_notes_plat_3:',
          'meta_notes_plat_4:',
          'meta_notes_plat_5:',
          'meta_notes_port_1:',
          'meta_notes_port_2:',
          'meta_notes_port_3:',
          'meta_notes_port_4:',
          'meta_notes_port_5:',
          'meta_notes_submit_1:',
          'meta_notes_submit_2:',
          'meta_notes_submit_3:',
          'meta_notes_submit_4:',
          'meta_notes_submit_5:',
          'meta_os:',
          'meta_provider:',
          'meta_provider_id:',
          'meta_region:',
          'meta_resource_id:',
          'meta_run_id:',
          'meta_storage_config:',
          'meta_sw_avail:',
          'meta_sw_other:',
          'meta_test_id:',
          'nobuild:',
          'nocleanup:',
          'nonuma:',
          'nosse_macro:',
          'output:',
          'purge_output',
          'rate',
          'reportable',
          'review',
          'run_timeout:',
          'size:',
          'spec_dir:',
          'sse:',
          'sse_max:',
          'sse_min:',
          'sse_skip:',
          'tune:',
          'validate_disk_space',
          'v' => 'verbose',
          'x64:',
          'x64_failover'
        );
        $this->options = parse_args($opts, array('benchmark'));
        $this->verbose = isset($this->options['verbose']);

        // set default parameter values
        foreach($defaults as $key => $val) {
          if (!isset($this->options[$key])) $this->options[$key] = $val;
        }
        
        // expand benchmarks (space and comma separated values)
        $benchmarks = array();
        if (isset($this->options['benchmark']) && !is_array($this->options['benchmark'])) $this->options['benchmark'] = array($this->options['benchmark']);
        if (isset($this->options['benchmark'])) {
          foreach($this->options['benchmark'] as $benchmark) {
            foreach(explode(' ', $benchmark) as $b1) {
              foreach(explode(',', $b1) as $b2) {
                if (!in_array(trim($b2), $benchmarks)) $benchmarks[] = trim($b2);
              }
            }
          }
        }
        $this->options['benchmark'] = $benchmarks;
        
        // determine copies
        $this->options['copies'] = $this->getNumCopies($this->options['copies'], isset($this->options['max_copies']) ? $this->options['max_copies'] : NULL);
        // automatically set to rate run if --copies > 1
        if ($this->options['copies'] > 1) $this->options['rate'] = 1;
        
        // merge multi-param notes
        foreach(array('', 'base', 'comp', 'os', 'part', 'peak', 'plat', 'port', 'submit') as $suffix) {
          $note = '';
          for($i=1; $i<=5; $i++) {
            $option = sprintf('meta_notes%s_%d', $suffix ? '_' . $suffix : '', $i);
            if (isset($this->options[$option])) $note .= ($note ? ' ' : '') . $this->options[$option];
          }
          if ($note) $this->options[sprintf('meta_notes%s', $suffix ? '_' . $suffix : '')] = $note;
        }
        
        // look up directory hierarchy for cpu2006 directory and in /opt/cpu2006
        if (!isset($this->options['spec_dir'])) {
          print_msg(sprintf('spec_dir directory not set - looking up directory hierarchy'), $this->verbose, __FILE__, __LINE__);
          $dirs = array($this->options['output']);
          if (($pwd = trim(shell_exec('pwd'))) != $this->options['output']) $dirs[] = $pwd;
          foreach($dirs as $dir) {
            while($dir != dirname($dir)) {
              if (is_dir($udir = sprintf('%s/%s', $dir, basename(self::SPEC_CPU_2006_DEFAULT_DIR)))) {
                print_msg(sprintf('spec_dir found in directory %s', $dir), $this->verbose, __FILE__, __LINE__);
                $this->options['spec_dir'] = $udir;
                break;
              }
              else print_msg(sprintf('spec_dir (%s) NOT found in directory %s', $udir, $dir), $this->verbose, __FILE__, __LINE__);
              $dir = dirname($dir);
            }
            if (isset($this->options['spec_dir'])) break;
          }
          if (!isset($this->options['spec_dir']) && is_dir(self::SPEC_CPU_2006_DEFAULT_DIR)) $this->options['spec_dir'] = self::SPEC_CPU_2006_DEFAULT_DIR;
          
          if (isset($this->options['spec_dir'])) print_msg(sprintf('spec_dir found in directory %s', $this->options['spec_dir']), $this->verbose, __FILE__, __LINE__);
          else print_msg(sprintf('spec_dir NOT found'), $this->verbose, __FILE__, __LINE__, TRUE);
        }
      }
    }
    
    return $this->options;
  }
  
  /**
   * returns options from the serialized file where they are written when a 
   * test completes
   * @param string $dir the directory where results were written to
   * @return array
   */
  public static function getSerializedOptions($dir) {
    return unserialize(file_get_contents(sprintf('%s/%s', $dir, self::SPEC_CPU_2006_TEST_OPTIONS_FILE_NAME)));
  }
  
  /**
   * returns a hash representing the macros that should be set using the runspec
   * --define argument - the possible macros are defined in the README under the
   * config runtime parameter documentation
   * @return array
   */
  private function getSpecMacros() {
    $macros = array();
    $sysInfo = get_sys_info();
		foreach(array('cpu_cache', 'cpu_count', 'cpu_family', 'cpu_model', 'cpu_name', 'cpu_speed', 
		              'cpu_vendor', 'compute_service_id', 'external_id', 'instance_id', 
		              'ip_or_hostname', 'is32bit', 'is64bit', 'iteration_num', 'mean_anyway', 'meta_hw_avail', 
		              'meta_hw_fpu', 'meta_hw_ncpuorder', 'meta_hw_nthreadspercore', 'meta_hw_other',
		              'meta_hw_ocache', 'meta_hw_pcache', 'meta_hw_tcache', 'meta_license_num',
		              'meta_notes_base', 'meta_notes_comp', 'meta_notes_comp', 'meta_notes',
		              'meta_notes_os', 'meta_notes_part', 'meta_notes_peak', 'meta_notes_plat',
		              'meta_notes_port', 'meta_notes_submit', 'meta_sw_avail', 'meta_sw_other',
		              'meta_tester', 'label', 'location', 'memory_free', 'memory_total', 'numa',
		              'os', 'os_version', 'provider_id', 'region', 'run_id', 'run_name', 
		              'storage_config', 'subregion', 'test_id', 'x64') as $macro) {

			// numa macro - determine if supported
			if ($macro == 'numa') $macros['numa'] = !$this->options['nonuma'] && (trim(exec('numactl --show 2>/dev/null; echo $?')) . '') === '0' ? TRUE : FALSE;
      
			// note parameters (up to 5)
			else if (preg_match('/^meta_notes/', $macro)) {
				for($i=1; $i<=5; $i++) {
					if (isset($this->options[$k = $macro . '_' . $i])) $macros[$k] = $this->options[$k];
				}
			}
			// 32 bit flag
			else if ($macro == 'is32bit') $macros[$macro] = !is_64bit();
			// 64 bit flag
			else if ($macro == 'is64bit') $macros[$macro] = is_64bit();
			// 32 or 64 bit binaries
			else if ($macro == 'x64') $macros[$macro] = is_64bit() && ($this->options['x64'] == 2 || $this->options['x64'] == 1) ? TRUE : FALSE;
			// other macros
			else {
			  $v = NULL;
			  if (isset($this->options[$macro])) $v = $this->options[$macro];
			  else if (isset($this->options['meta_' . $macro])) $v = $this->options['meta_' . $macro];
			  if (isset($sysInfo[$macro])) $v = $sysInfo[$macro];
			  else if ($macro == 'cpu_count' && isset($sysInfo['cpu_cores'])) $v = $sysInfo['cpu_cores'];
			  else if (!($v = getenv("bm_param_${macro}"))) $v = getenv("bm_${macro}");
			  if ($v !== NULL) $macros[$macro] = $v;
			}
		}
		// SSE
		if ($sse = $this->getSse()) $macros['sse'] = str_replace('sse4.2', 'sse42', str_replace('sse4_2', 'sse42', strtolower($sse)));
  	else if (isset($this->options['sse']) && $this->options['sse'] == 'optimal' && isset($this->options['nosse_macro']) && $this->options['nosse_macro']) {
  	  print_msg(sprintf('Adding --nosse_macro %s macro for --sse optimal and no SSE flag', $this->options['nosse_macro']), $this->verbose, __FILE__, __LINE__);
  	  $macros[$this->options['nosse_macro']] = TRUE;
  	}
		// Additional macros (define_* parameters)
    foreach(get_prefixed_params('define_') as $key => $val) $macros[$key] = $val;
    
    // SSE failover
  	if (file_exists(sprintf('%s/%s', $this->options['output'], self::SPEC_CPU_2006_SKIP_SSE_FILE)) && isset($macros['sse'])) unset($macros['sse']);
  	// x64 failover
  	if (file_exists(sprintf('%s/%s', $this->options['output'], self::SPEC_CPU_2006_X64_FAILOVER_FILE)) && isset($macros['sse'])) $macros['x64'] = !$macros['x64'];
    
    print_msg(sprintf('Applying runspec macros [%s] => [%s]', implode(', ', array_keys($macros)), implode(', ', $macros)), $this->verbose, __FILE__, __LINE__);

  	return $macros;
  }
  
  /**
   * returns the SSE flag ot use for this run
   * @return string
   */
  private function getSse() {
  	// skip sse
  	if ($this->options['failover_no_sse'] && file_exists(sprintf('%s/%s', $this->options['output'], self::SPEC_CPU_2006_SKIP_SSE_FILE))) {
  		print_msg('SSE will not be used because the --failover_no_sse was set and the previous run failed', $this->verbose, __FILE__, __LINE__);
  		return NULL;
  	}
    
  	$sse_flags = array('SSE2', 'SSE3', 'SSSE3', 'SSE4.1', 'SSE4.2', 'AVX', 'AVX2');
  	$sse = $this->options['sse'];
  	$skip_sse = isset($this->options['sse_skip']) ? $this->options['sse_skip'] : NULL;
  	
  	if ($sse == 'none') return NULL;
  	else if ($sse == 'optimal') {
  	  foreach($sse_flags as $flag) {
  	    if ($skip_sse == $flag) continue;
  	    
  	    $ecode = (exec(sprintf('cat /proc/cpuinfo | grep %s >/dev/null; echo $?', str_replace('.', '_', strtolower($flag)))))*1;
  	    if ($ecode === 0) {
  	      $sse = $flag;
  	      print_msg(sprintf('SSE flag %s is supported', $flag), $this->verbose, __FILE__, __LINE__);
  	    }
  	    else {
  	      print_msg(sprintf('Aborting optimal SSE search because SSE flag %s is not supported', $flag), $this->verbose, __FILE__, __LINE__);
  	      break;
	      }
  	  }
      if ($sse == 'optimal') $sse = NULL;
      print_msg(sprintf('Using optimal SSE flag %s', $sse ? $sse : 'none'), $this->verbose, __FILE__, __LINE__);
	  }
  	if ($sse) $sse_pos = array_search($sse, $sse_flags);

  	$min_sse = isset($this->options['sse_min']) ? array_search($this->options['sse_min'], $sse_flags) : FALSE;
  	$max_sse = isset($this->options['sse_max']) ? array_search($this->options['sse_max'], $sse_flags) : FALSE;

  	if ($min_sse !== FALSE && $sse && $sse_pos < $min_sse) {
  	  print_msg(sprintf('SSE %s does not meet --sse_min %s - SSE will not be used', $sse, $this->options['sse_min']), $this->verbose, __FILE__, __LINE__);
  		$sse = NULL;
  	}
  	if ($max_sse !== FALSE && $sse && $sse_pos > $max_sse) {
  	  print_msg(sprintf('SSE %s is greater than --sse_max %s - changing to %s', $sse, $this->options['sse_max'], $this->options['sse_max']), $this->verbose, __FILE__, __LINE__);
  		$sse = $this->options['sse_max'];
  	}

  	return $sse;
  }
  
  /**
   * Moves PDF, CSV, HTML, Text and GIF SPEC CPU 2006 result files to the 
   * output directory. Also modifies HTML to reference CSS files hosted on 
   * cloudharmony.com and new GIF file names
   * @return boolean
   */
  function moveResultFiles($log) {
    $moved = FALSE;
  	if (preg_match_all('/format: (.*) -> (.*)$/msU', file_get_contents($log), $m)) {
  	  print_msg(sprintf('Moving and renaming output files [%s] to directory %s', implode(', ', $m[1]), $this->options['output']), $this->verbose, __FILE__, __LINE__);
  	  $id = NULL;
  		foreach($m[1] as $i => $key) {
  			$key = strtolower(trim($key));
  			foreach(explode(',', $m[2][$i]) as $file) {
  				if (($file = trim($file)) && file_exists($file)) {
  				  if (!preg_match('/CINT/', basename($file)) && !preg_match('/CFP/', basename($file))) continue;
            $fp = preg_match('/CFP/', basename($file));
            $pieces = explode('.', $file);
            $id = $pieces[1];
            $type = $pieces[count($pieces) - 1];
            if (in_array($type, array('pdf', 'csv', 'html', 'txt', 'gif'))) {
              $path = sprintf('%s/spec%s2006.%s', $this->options['output'], $fp ? 'fp' : 'int', $type);
              exec(sprintf('mv %s %s', $file, $path));
              if (file_exists($path)) {
                if ($type == 'csv') $moved = TRUE;
                print_msg(sprintf('Successfully moved output file %s to %s', $file, $path), $this->verbose, __FILE__, __LINE__);
                // update references to css files and gif in the html report
                if ($type == 'html') {
                  $html = file_get_contents($path);
                  $html = str_replace('http://www.spec.org/includes/css/', '//cloudharmony.com/assets/cpu2006/', $html);
                  $html = str_replace('body { background-image: url(invalid.gif) }', '', $html);
                  if (preg_match('/img src="(.*).gif"/', $html, $m1)) $html = str_replace($m1[1] . '.gif', sprintf('spec%s2006.gif', $fp ? 'fp' : 'int'), $html);
                  $fp = fopen($path, 'w');
                  fwrite($fp, $html);
                  fclose($fp);
                }
              }
              else print_msg(sprintf('Unable to move output file %s to %s', $file, $path), $this->verbose, __FILE__, __LINE__, TRUE);
            }
            else {
              print_msg(sprintf('Removing output file %s', $file), $this->verbose, __FILE__, __LINE__);
              exec(sprintf('rm -f %s', $file));
            }
  				}
  			}
  		}
  		// check for CPU2006.[id].log
  		if ($id && file_exists($file = sprintf('%s/result/CPU2006.%s.log', $this->options['spec_dir'], $id))) {
        print_msg(sprintf('Removing CPU2006 log file %s, invalid.gif and lock.CPU2006', $file), $this->verbose, __FILE__, __LINE__);
        exec(sprintf('rm -f %s', $file));
        exec(sprintf('rm -f %s/invalid.gif', dirname($file)));
        exec(sprintf('rm -f %s/lock.CPU2006', dirname($file)));
  		}
  		else print_msg(sprintf('Unable to remove CPU2006 log file %s', $file), $this->verbose, __FILE__, __LINE__);
  	}
  	else print_msg(sprintf('No output files found in %s', $log), $this->verbose, __FILE__, __LINE__, TRUE);
  	
  	return $moved;
  }
  
  /**
   * initiates stream scaling testing. returns TRUE on success, FALSE otherwise
   * @return boolean
   */
  public function test() {
    $rrdStarted = isset($this->options['collectd_rrd']) ? ch_collectd_rrd_start($this->options['collectd_rrd_dir'], isset($this->options['verbose'])) : FALSE;
    $success = FALSE;
    
    $this->getRunOptions();
    ini_set('max_execution_time', $this->options['run_timeout']);
    $this->options['test_started'] = date('Y-m-d H:i:s');
    
    // setup runspec command
    $config = $this->options['config'] != 'default.cfg' ? $this->options['config'] : NULL;
    $benchmarks = implode(' ', $this->options['benchmark']);
    $macros = $this->getSpecMacros();
    $comment = isset($this->options['comment']) ? $this->options['comment'] : NULL;
    $delay = isset($this->options['delay']) ? $this->options['delay'] : NULL;
    $flagsurl = isset($this->options['flagsurl']) ? $this->options['flagsurl'] : NULL;
    $ignoreErrors = $this->options['ignore_errors'] == 1;
    $iterations = $this->options['iterations'] != 3 ? $this->options['iterations'] : NULL;
    $nobuild = $this->options['nobuild'] == 1;
    $rate = $this->options['rate'] == 1;
    $reportable = $this->options['reportable'] == 1;
    $review = $this->options['review'] == 1;
    $size = $this->options['size'] != 'ref' ? $this->options['size'] : NULL;
    $tune = $this->options['tune'];

  	$runspec = '--output_format=all';
  	$runspec .= $reportable ? ' --reportable' : ' --noreportable';
  	if ($config) $runspec .= " --config=${config}";
  	if ($comment) $runspec .= ' --comment="' . $comment . '"';
  	if ($delay) $runspec .= " --delay=${delay}";
  	if ($flagsurl) $runspec .= ' --flagsurl="' . $flagsurl . '"';
  	if ($ignoreErrors) $runspec .= " --ignore_errors";
  	if ($iterations) $runspec .= " --iterations=${iterations}";
  	if ($nobuild) $runspec .= " --nobuild";
  	if ($rate) $runspec .= " --rate " . $this->options['copies'];
  	if ($review) $runspec .= " --review";
  	if ($size) $runspec .= " --size=${size}";
  	if ($tune) $runspec .= " --tune=${tune}";
  	
  	$utilPatched = preg_match('/"\'\.\$macros->{\$macro}\.\'"\'/msU', file_get_contents($this->options['spec_dir'] . '/bin/util.pl'));
  	$sseFlag = NULL;
  	foreach($macros as $key => $val) {
  		if ($val === FALSE) continue;
  		print_msg(sprintf('Adding custom macro %s=%s', $key, $val), $this->verbose, __FILE__, __LINE__);

  		// skip macros with spaces, dashes or periods if bin/util.pl has not been patched
  		if ($key != 'sse' && !$utilPatched && $val && (strpos($val, ' ') || strpos($val, '-') || strpos($val, '.'))) continue;
      
  		$runspec .= ($p = ' --define ' . $key . (!is_bool($val) && $val != '1' ? '=' . (strpos(' ', $val) ? sprintf('"%s"', $val) : $val) : ''));
  		if ($key == 'sse') $sseFlag = $p;
  	}
  	if ($rate) $runspec .= ' --define rate=' . $this->options['copies'];
  	$runspec .= " ${benchmarks}";
    
    $sseX64 = array(array('sse' => $macros['sse'], 'x64' => $macros['x64']));
    if ($this->options['x64_failover']) $sseX64[] = array('sse' => $macros['sse'], 'x64' => !$macros['x64']);
    if ($macros['sse'] && $this->options['failover_no_sse']) $sseX64[] = array('sse' => NULL, 'x64' => $macros['x64']);
    if ($macros['sse'] && $this->options['x64_failover']) $sseX64[] = array('sse' => NULL, 'x64' => !$macros['x64']);
    // try to run spec for every combination of sse and x64
    foreach($sseX64 as $options) {
      $sse = $options['sse'];
      $x64 = $options['x64'];
      $cmd = $runspec;
      if ($sseFlag && !$sse) $cmd = str_replace($sseFlag, '', $cmd);
      if ($macros['x64'] != $x64) $cmd = str_replace($macros['x64'] ? ' --define x64' : 'runspec ', $macros['x64'] ? '' : 'runspec --define x64 ', $cmd);
      if ($script = $this->generateRunScript($cmd, $sse, $x64)) {
        print_msg(sprintf('Attempting to run SPEC CPU 2006 using options [SSE=%s; X64=%d] and script %s', $sse, $x64, $script), $this->verbose, __FILE__, __LINE__);
        $ofile = sprintf('%s/%s.out', $this->options['output'], basename($script));
        $efile = sprintf('%s/%s.err', $this->options['output'], basename($script));
        $xfile = sprintf('%s/%s.status', $this->options['output'], basename($script));
        $cmd = sprintf('%s | tee %s 2>%s;echo $? > %s', $script, $ofile, $efile, $xfile);
        
        passthru($cmd);
        $ecode = trim(file_get_contents($xfile));
        $ecode = strlen($ecode) && is_numeric($ecode) ? $ecode*1 : NULL;
        unlink($xfile);
        // remove error file if it is empty
        if (file_exists($efile) && !filesize($efile)) unlink($efile);
        $spec = file_get_contents($ofile);
        
        // non-zero exit code
        if ($ecode && !$ignoreErrors) print_msg(sprintf('Unable to run SPEC CPU 2006 using options [SSE=%s; X64=%d] - exit code %d. Check stderr file %s', $sse, $x64, $ecode, $efile), $this->verbose, __FILE__, __LINE__, TRUE);
        else {
          if (strpos($spec, 'exit code=1')) print_msg(sprintf('One or more benchmarks exited with a non-zero exit code. Check stdout file %s', $ofile), $this->verbose, __FILE__, __LINE__, TRUE);
          if (strpos($spec, 'NOT Building')) print_msg(sprintf('One more more SPEC CPU 2006 benchmark binaries were not present using options [SSE=%s; X64=%d]. Check stdout file %s', $sse, $x64, $ofile), $this->verbose, __FILE__, __LINE__, TRUE);
          $specFile = sprintf('%s/%s', $this->options['output'], self::SPEC_CPU_2006_RUN_OUTPUT_FILE);
          exec(sprintf('cp %s %s', $ofile, $specFile));
          if ($success = $this->moveResultFiles($ofile)) {
            $this->options['sse'] = $sse;
            $this->options['x64'] = $x64;
            print_msg(sprintf('SPEC CPU 2006 run finished successfully - results written to %s', $specFile), $this->verbose, __FILE__, __LINE__);
            $this->endTest();
          }
          else print_msg(sprintf('SPEC CPU 2006 run finished successfully - but did not generate a CSV output file using options [SSE=%s; X64=%d]', $sse, $x64), $this->verbose, __FILE__, __LINE__, TRUE);
        }
        // cleanup test files
        foreach($this->getBenchmarks() as $benchmark) {
          $dir = sprintf('%s/benchspec/CPU2006/%s/run', $this->options['spec_dir'], $benchmark);
          if ($this->options['nocleanup']) print_msg(sprintf('Skipping cleanup up test directories in %s', $dir), $this->verbose, __FILE__, __LINE__);
          else {
            print_msg(sprintf('Cleaning up test directories in %s', $dir), $this->verbose, __FILE__, __LINE__);
            exec(sprintf('rm -rf %s/*', $dir)); 
          }
        }
        if ($success) break;
      }
      else {
        print_msg(sprintf('Run failed - unable to generate SPEC CPU 2006 run script for options [SSE=%s; X64=%d]', $sse, $x64), $this->verbose, __FILE__, __LINE__, TRUE);
        break;
      }
    }
    if ($rrdStarted) ch_collectd_rrd_stop($this->options['collectd_rrd_dir'], $this->options['output'], isset($this->options['verbose']));
    
    return $success;
  }
  
  /**
   * validates there is sufficient disk space to run SPEC CPU. Returns TRUE if
   * it is sufficient, FALSE otherwise
   * @return boolean
   */
  private function validateDiskSpace() {
    $valid = FALSE;
    if (is_dir($this->options['spec_dir']) && isset($this->options['copies'])) {
      print_msg(sprintf('Validating disk space for %d copies on volume where %s resides', $this->options['copies'], $this->options['spec_dir']), $this->verbose, __FILE__, __LINE__);
      $free = get_free_space($this->options['spec_dir']);
      $required = $this->options['copies']*self::SPEC_CPU_2006_ESTIMATED_DISK_USAGE_MB;
			if ($valid = $free >= $required) print_msg(sprintf('Required disk space %sMB is available (%sMB is available)', $required, $free), $this->verbose, __FILE__, __LINE__);
			else print_msg(sprintf('Required disk space %sMB is NOT available (%sMB is available)', $required, $free), $this->verbose, __FILE__, __LINE__, TRUE);
    }
    return $valid;
  }
  
  /**
   * validate run options. returns an array populated with error messages 
   * indexed by the argument name. If options are valid, the array returned
   * will be empty
   * @return array
   */
  public function validateRunOptions() {
    $this->getRunOptions();
    $validate = array(
      'benchmark' => array('option' => array('all', 'int', 'fp', '400', '400.perlbench', '401', '401.bzip2', '403', '403.gcc', '429', '429.mcf', '445', '445.gobmk', '456', '456.hmmer', '458', '458.sjeng', '462', '462.libquantum', '464', '464.h264ref', '471', '471.omnetpp', '473', '473.astar', '483', '483.xalancbmk', '410', '410.bwaves', '416', '416.gamess', '433', '433.milc', '434', '434.zeusmp', '435', '435.gromacs', '436', '436.cactusADM', '437', '437.leslie3d', '444', '444.namd', '447', '447.dealII', '450', '450.soplex', '453', '453.povray', '454', '454.calculix', '459', '459.GemsFDTD', '465', '465.tonto', '470', '470.lbm', '481', '481.wrf', '482', '482.sphinx3', '-400', '-400.perlbench', '-401', '-401.bzip2', '-403', '-403.gcc', '-429', '-429.mcf', '-445', '-445.gobmk', '-456', '-456.hmmer', '-458', '-458.sjeng', '-462', '-462.libquantum', '-464', '-464.h264ref', '-471', '-471.omnetpp', '-473', '-473.astar', '-483', '-483.xalancbmk', '-410', '-410.bwaves', '-416', '-416.gamess', '-433', '-433.milc', '-434', '-434.zeusmp', '-435', '-435.gromacs', '-436', '-436.cactusADM', '-437', '-437.leslie3d', '-444', '-444.namd', '-447', '-447.dealII', '-450', '-450.soplex', '-453', '-453.povray', '-454', '-454.calculix', '-459', '-459.GemsFDTD', '-465', '-465.tonto', '-470', '-470.lbm', '-481', '-481.wrf', '-482', '-482.sphinx3'), 'required' => TRUE),
      'copies' => array('min' => 1, 'required' => TRUE),
      'failover_no_sse' => array('min' => 0, 'max' => 1, 'required' => TRUE),
      'huge_pages' => array('min' => 0, 'max' => 1, 'required' => TRUE),
      'ignore_errors' => array('min' => 0, 'max' => 1, 'required' => TRUE),
      'iterations' => array('min' => 1, 'max' => 10),
      'max_copies' => array('min' => 1),
      'nobuild' => array('min' => 0, 'max' => 1, 'required' => TRUE),
      'nocleanup' => array('min' => 0, 'max' => 1, 'required' => TRUE),
      'nonuma' => array('min' => 0, 'max' => 1, 'required' => TRUE),
      'output' => array('write' => TRUE, 'required' => TRUE),
      'purge_output' => array('min' => 0, 'max' => 1, 'required' => TRUE),
      'rate' => array('min' => 0, 'max' => 1, 'required' => TRUE),
      'reportable' => array('min' => 0, 'max' => 1, 'required' => TRUE),
      'review' => array('min' => 0, 'max' => 1, 'required' => TRUE),
      'run_timeout' => array('min' => 3600, 'required' => TRUE),
      'size' => array('option' => array('test', 'train', 'ref')),
      'spec_dir' => array('write' => TRUE, 'required' => TRUE),
      'sse' => array('option' => array('none', 'AVX2', 'AVX', 'SSE4.2', 'SSE4.1', 'SSSE3', 'SSE3', 'SSE2', 'optimal'), 'required' => TRUE),
      'sse_max' => array('option' => array('AVX2', 'AVX', 'SSE4.2', 'SSE4.1', 'SSSE3', 'SSE3', 'SSE2')),
      'sse_min' => array('option' => array('AVX2', 'AVX', 'SSE4.2', 'SSE4.1', 'SSSE3', 'SSE3', 'SSE2')),
      'sse_skip' => array('option' => array('AVX2', 'AVX', 'SSE4.2', 'SSE4.1', 'SSSE3', 'SSE3', 'SSE2')),
      'tune' => array('option' => array('base', 'peak', 'all')),
      'validate_disk_space' => array('min' => 0, 'max' => 1, 'required' => TRUE),
      'x64' => array('min' => 0, 'max' => 2, 'required' => TRUE),
      'x64_failover' => array('min' => 0, 'max' => 1, 'required' => TRUE)
    );
    $validated = validate_options($this->options, $validate);
    
    if (!is_array($validated)) $validated = array();
    
    // convert benchmark parameter (remove '.[benchmark]' suffix)
    if (!isset($validated['benchmark'])) {
      $config = string_to_hash(file_get_contents(dirname(dirname(__FILE__)) . '/config/spec-benchmarks.ini'), TRUE);
      // expand all/int/fp identifiers
      foreach(array('int' => 'cint2006', 'fp' => 'cfp2006') as $id => $key) {
        if (in_array($id, $this->options['benchmark']) || in_array('all', $this->options['benchmark'])) {
          foreach(array_keys($config[$key]) as $benchmark) $this->options['benchmark'][] = $benchmark;
        }
      }
      // remove group identifiers and .[benchmark] suffix
      $remove = array();
      foreach($this->options['benchmark'] as $i => $benchmark) {
        if ($benchmark == 'all' || $benchmark == 'int' || $benchmark == 'fp') unset($this->options['benchmark'][$i]);
        else {
          $pieces = explode('.', $benchmark);
          if (substr($pieces[0], 0, 1) == '-') {
            $remove[] = substr($pieces[0], 1);
            unset($this->options['benchmark'][$i]);
          }
          else $this->options['benchmark'][$i] = $pieces[0];
        }
      }
      // remove benchmarks designated with minus prefix
      $this->options['benchmark'] = array_unique($this->options['benchmark']);
      foreach($remove as $check) {
        foreach($this->options['benchmark'] as $i => $benchmark) if ($check == $benchmark) unset($this->options['benchmark'][$i]);
      }
      // re-add group identifiers
      if (count($this->options['benchmark']) == (count($config['cint2006']) + count($config['cfp2006']))) $this->options['benchmark'] = array('all');
      else {
        foreach(array('int' => 'cint2006', 'fp' => 'cfp2006') as $id => $key) {
          $missing = FALSE;
          foreach(array_keys($config[$key]) as $benchmark) {
            $pieces = explode('.', $benchmark);
            if (!in_array($pieces[0], $this->options['benchmark'])) $missing = TRUE;
          }
          if (!$missing) {
            $this->options['benchmark'][] = $id;
            foreach(array_keys($config[$key]) as $benchmark) {
              $pieces = explode('.', $benchmark);
              unset($this->options['benchmark'][array_search($pieces[0], $this->options['benchmark'])]);
            }
          }
        }
      }
      sort($this->options['benchmark']);
      if (!count($this->options['benchmark'])) $validated['benchmark'] = 'You must specify at least 1 benchmark';
    }
    
    // validate config and flagsurl
    if (!isset($validated['spec_dir'])) {
      // validate config directory present
      if (!is_dir($cdir = sprintf('%s/config', $this->options['spec_dir']))) $validated['spec_dir'] = sprintf('Config directory %s is not present', $cdir);
      else if (!is_writable($cdir)) $validated['spec_dir'] = sprintf('Config directory %s is not writable', $cdir);
      
      // if --config not specified, default.cfg must exist
      if (!isset($this->options['config']) && !file_exists($def = sprintf($f = '%s/config/default.cfg', $this->options['spec_dir']))) $validated['config'] = sprintf('--config not specified and %s does not exist', $def);
      // --config references a file outside of [spec_dir]/config => copy file into that location
      else if (isset($this->options['config']) && !file_exists($f = sprintf('%s/config/%s', $this->options['spec_dir'], $this->options['config']))) {
        $ofile = sprintf('%s/config/%s', $this->options['spec_dir'], basename($this->options['config']));
        print_msg(sprintf('Attempting to copy SPEC config from %s to %s because %s does not exist', $this->options['config'], $ofile, $f), $this->verbose, __FILE__, __LINE__);
        if ($fp = @fopen($this->options['config'], 'r')) {
          if ($cfp = fopen($ofile, 'w')) {
            while($line = fgets($fp)) fwrite($cfp, $line);
            fclose($cfp);
            $this->options['config'] = basename($this->options['config']);
          }
          else $validated['config'] = sprintf('Unable to copy config %s to %s', $this->options['config'], $ofile);
          fclose($fp);
        }
        else $validated['config'] = sprintf('%s is not valid', $this->options['config']);
      }
      
      // validate flagsurl
      if (isset($this->options['flagsurl']) && !file_exists(sprintf('%s/config/flags/%s', $this->options['spec_dir'], $this->options['flagsurl']))) {
        if ($fp = fopen($file = str_replace('$[top]', $this->options['spec_dir'], $this->options['flagsurl']), 'r')) fclose($fp);
        else $validated['flagsurl'] = sprintf('%s [%s] is not valid', $this->options['flagsurl'], $file);
      }
      
      // validate free disk space
      if ($this->options['validate_disk_space'] && !$this->validateDiskSpace()) $validated['spec_dir'] = 'There is insufficient disk space';
    }
    // validate huge_pages
    if ($this->options['huge_pages'] && !file_exists(self::SPEC_CPU_2006_HUGE_PAGES_LIB64) && !file_exists(self::SPEC_CPU_2006_HUGE_PAGES_LIB32)) $validated['huge_pages'] = sprintf('--huge_pages set but /usr/lib64/libhugetlbfs.so and /usr/lib/libhugetlbfs.so are not present');
    
    // validate collectd rrd options
    if (isset($this->options['collectd_rrd'])) {
      if (!ch_check_sudo()) $validated['collectd_rrd'] = 'sudo privilege is required to use this option';
      else if (!is_dir($this->options['collectd_rrd_dir'])) $validated['collectd_rrd_dir'] = sprintf('The directory %s does not exist', $this->options['collectd_rrd_dir']);
      else if ((shell_exec('ps aux | grep collectd | wc -l')*1 < 2)) $validated['collectd_rrd'] = 'collectd is not running';
      else if ((shell_exec(sprintf('find %s -maxdepth 1 -type d 2>/dev/null | wc -l', $this->options['collectd_rrd_dir']))*1 < 2)) $validated['collectd_rrd_dir'] = sprintf('The directory %s is empty', $this->options['collectd_rrd_dir']);
    }
    
    return $validated;
  }
  
}
?>
